fix(extensions): guard non-numeric catalog downloads in search/info rendering#3710
fix(extensions): guard non-numeric catalog downloads in search/info rendering#3710jawwad-ali wants to merge 3 commits into
Conversation
…endering `specify extension search` and `specify extension info <id>` format a catalog entry's `downloads` field with the `:,` thousands separator, guarded only by `is not None`. Catalog payloads are only shape-validated -- individual fields are never type-checked and `_get_merged_extensions` returns raw catalog dicts -- so an entry with a non-numeric `downloads` (e.g. the JSON string "1500", realistic from a community / SPECKIT_CATALOG_URL / project catalog) makes the `:,` format raise `ValueError: Cannot specify ',' with 's'`, aborting the whole command with an uncaught traceback. Group-format `downloads` only when it is actually numeric; otherwise render it as-is. Numeric values (int/float, incl. bool) format identically, so correct catalogs are byte-for-byte unchanged. Every other field in these two renderers is already `str()`-wrapped; this closes the one unguarded field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Guards extension catalog rendering against non-numeric download counts.
Changes:
- Formats numeric downloads with separators and renders other values directly.
- Adds coverage for search and info rendering.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions/_commands.py |
Adds defensive download rendering. |
tests/test_extensions.py |
Tests string-valued downloads. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
src/specify_cli/extensions/_commands.py:991
- The info renderer also passes the untrusted fallback directly to Rich. For example,
downloads="[/red]foo"still produces an uncaughtMarkupError, while valid markup can inject styling or links. Escape the string fallback so arbitrary non-numeric catalog values actually render safely.
stats.append(
f"Downloads: {downloads:,}"
if isinstance(downloads, (int, float))
else f"Downloads: {downloads}"
)
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Medium
…arkup Address review feedback: the fallback interpolated the untrusted catalog value straight into a Rich-rendered string, so guarding the ``:,`` ValueError just traded it for a MarkupError -- a catalog entry with downloads "[/red]foo" still aborted `extension search`/`info`, and balanced tags could restyle the output. Wrap the fallback in _escape_markup(str(...)) at both sites, matching how every other catalog field in these renderers is already escaped. Numeric values keep the identical ``:,`` branch, so correct catalogs are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You're right — fixed in 135444f. Guarding the Both sites now escape the fallback — Tests parametrized over |
Follow-up to the downloads escaping: `stars` is the other catalog-controlled value joined into the same Rich-rendered stats line, and it was still raw -- verified that stars "[/red]x" raises the same MarkupError and aborts `extension info`/`search`. Hardening one of the two adjacent values would have left the reported defect reachable through the sibling field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
specify extension searchandspecify extension info <id>render a catalog entry'sdownloadsfield with the:,thousands separator:guarded only by
is not None. But:,is only valid forint/float— on a string it raisesValueError: Cannot specify ',' with 's'.Catalog payloads are only shape-validated (
_validate_catalog_payloadchecks thatextensionsis a dict); individual entry fields are never type-checked, and_get_merged_extensionsreturns the raw catalog dicts. So a catalog entry with"downloads": "1500"(a quoted number — realistic from a community catalog, aSPECKIT_CATALOG_URLcatalog, a project/user catalog, or a poisoned cache) makes the whole command abort with an uncaught traceback. Reproduced end-to-end: with such a cache, bothextension searchandextension infoexit non-zero withValueError("Cannot specify ',' with 's'.").Fix
Group-format
downloadsonly when it is actually numeric; otherwise render it as-is:(and the equivalent in
_print_extension_info). Numeric values (int/float, includingbool) take the identical:,branch, so correct catalogs render byte-for-byte the same — only the previously-crashing case changes to print the value. This mirrors the module's existing defensive treatment: every other field in these two renderers is alreadystr()-wrapped.Verification
test_info_renders_non_numeric_downloads(direct) andtest_search_survives_non_numeric_downloads(end-to-end viaCliRunner): both fail before with the exactValueError, pass after (exit_code == 0, output showsDownloads: 1500).TestExtensionCatalog: 64 passed.ruffclean.AI-assisted: authored with Claude Code. I reproduced the crash on
mainthrough the real CLI (catalog cache with a stringdownloads) before writing the guard, and confirmed numeric output is unchanged.